/*
 *   Copyright 2012, Thomas Kerber
 *
 *   Licensed under the Apache License, Version 2.0 (the "License");
 *   you may not use this file except in compliance with the License.
 *   You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing, software
 *   distributed under the License is distributed on an "AS IS" BASIS,
 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *   See the License for the specific language governing permissions and
 *   limitations under the License.
 */
package milk.jpatch;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Logger;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

import org.apache.bcel.Constants;
import org.apache.bcel.classfile.AnnotationElementValue;
import org.apache.bcel.classfile.AnnotationEntry;
import org.apache.bcel.classfile.ArrayElementValue;
import org.apache.bcel.classfile.ClassElementValue;
import org.apache.bcel.classfile.Code;
import org.apache.bcel.classfile.Constant;
import org.apache.bcel.classfile.ConstantCP;
import org.apache.bcel.classfile.ConstantClass;
import org.apache.bcel.classfile.ConstantNameAndType;
import org.apache.bcel.classfile.ConstantPool;
import org.apache.bcel.classfile.ConstantUtf8;
import org.apache.bcel.classfile.ElementValue;
import org.apache.bcel.classfile.ElementValuePair;
import org.apache.bcel.classfile.EnumElementValue;
import org.apache.bcel.classfile.InnerClass;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.Method;
import org.apache.bcel.classfile.SimpleElementValue;
import org.apache.bcel.generic.Type;

/**
 * Provides utility methods.
 * 
 * @author Thomas Kerber
 * @version 1.0.6
 */
public class Util{
    
    /**
     * The global logger for jpatch.
     */
    public static final Logger logger = Logger.getLogger("milk.jpatch");
    
    /**
     * 
     * @param a An annotation entry.
     * @return Its length in bytes.
     */
    public static int getLength(AnnotationEntry a){
        int length = 4; // type_index / num_element_value_pairs
        for(ElementValuePair e : a.getElementValuePairs()){
            length += 2; // element_name_index;
            length += getLength(e.getValue());
        }
        return length;
    }
    
    /**
     * 
     * @param ev An Element/Value pair
     * @return Its length in bytes.
     */
    public static int getLength(ElementValue ev){
        int length = 1; // tag
        if(ev instanceof SimpleElementValue)
            length += 2; // const_value_index
        else if(ev instanceof EnumElementValue)
            length += 4; // type_name_index, const_name_index
        else if(ev instanceof ClassElementValue)
            length += 2; // class_info_index
        else if(ev instanceof AnnotationElementValue)
            length += getLength(((AnnotationElementValue)ev).
                    getAnnotationEntry());
        else if(ev instanceof ArrayElementValue){
            length += 2; // num_values
            ArrayElementValue aev = (ArrayElementValue)ev;
            for(ElementValue ev2 : aev.getElementValuesArray())
                length += getLength(ev2);
        }
        
        return length;
    }
    
    /**
     * A stand-in equals method.
     * @param first
     * @param second
     * @return
     */
    public static boolean equals(AnnotationEntry first,
            AnnotationEntry second){
        if(first == second)
            return true;
        if(first == null || second == null)
            return false;
        return first.toShortString().equals(second.toShortString());
    }
    
    /**
     * A stand-in equals method.
     * @param first
     * @param second
     * @param firstCP The constant pool belonging to the first inner class.
     * @param secondCP The constant pool belonging to the second inner class.
     * @return
     */
    public static boolean equals(InnerClass first, InnerClass second,
            ConstantPool firstCP, ConstantPool secondCP){
        if(first == second)
            return true;
        if(first == null || second == null)
            return false;
        
        String innerClassFirst = firstCP.constantToString(
                firstCP.getConstant(first.getInnerClassIndex()));
        String innerClassSecond = secondCP.constantToString(
                secondCP.getConstant(second.getInnerClassIndex()));

        String outerClassFirst = firstCP.constantToString(
                firstCP.getConstant(first.getOuterClassIndex()));
        String outerClassSecond = secondCP.constantToString(
                secondCP.getConstant(second.getOuterClassIndex()));

        String nameFirst = firstCP.constantToString(
                firstCP.getConstant(first.getInnerNameIndex()));
        String nameSecond = secondCP.constantToString(
                secondCP.getConstant(second.getInnerNameIndex()));
        
        return innerClassFirst.equals(innerClassSecond) &&
                outerClassFirst.equals(outerClassSecond) &&
                nameFirst.equals(nameSecond) &&
                first.getInnerAccessFlags() == second.getInnerAccessFlags();
    }
    
    /**
     * A stand-in, temporary equals method.
     * 
     * This method will be rendered obsolete by opcode diffs.
     * @param c1
     * @param c2
     * @param map The CPoolMap for mapping from c2's environment to c1's.
     * @return
     */
    public static boolean equals(Code c1, Code c2, CPoolMap map){
        if(c1 == c2)
            return true;
        if(c1 == null || c2 == null)
            return false;
        c2 = map.applyTo(c2);
        return Arrays.equals(c1.getCode(), c2.getCode());
        // TODO: this is to be removed once opcode modification patches are
        // added.
    }
    
    /**
     * 
     * @param map The cpool map (Convenience instead of passing map.new_)
     * @param str The string to search for. Note: It must be guaranteed that
     *     the string will be found.
     * @return The strings index in the constant pool.
     */
    public static int findConstantStringIn(CPoolMap map, String str){
        Constant[] constants = map.to.getConstantPool();
        for(int i = 1; i < constants.length; i++){
            Constant c = constants[i];
            if(c instanceof ConstantUtf8 &&
                    ((ConstantUtf8)c).getBytes().equals(str))
                return i;
        }
        throw new IllegalStateException(
                "Expected name (" + str + ") not found in constant pool.");
    }
    
    /**
     * 
     * @param map The cpool map (Convenience instead of passing map.new_)
     * @param name The class name to search for. Note: It mustbe guaranteed
     *     that the corresponding class will be found.
     * @return the classes index in the constant pool.
     */
    public static int findConstantClassIn(CPoolMap map, String name){
        return findConstantClassIn(map, name, false);
    }
    
    /**
     * 
     * @param map The cpool map (Convenience instead of passing map.new_)
     * @param name The class name to search for. Note: It mustbe guaranteed
     *     that the corresponding class will be found.
     * @param acceptError Whether or not to throw an exception if no result was
     *     found. Returns -1 else.
     * @return the classes index in the constant pool.
     */
    public static int findConstantClassIn(CPoolMap map, String name,
            boolean acceptError){
        Constant[] constants = map.to.getConstantPool();
        for(int i = 1; i < constants.length; i++){
            Constant c = constants[i];
            if(c instanceof ConstantClass &&
                    ((ConstantClass)c).getBytes(map.to).equals(name))
                return i;
        }
        if(acceptError)
            return -1;
        throw new IllegalStateException(
                "Expected name (" + name + ") not found in constant pool.");
    }
    
    /**
     * Gets the signature of a method from its identifier and returner.
     * 
     * @param identifier The methods identifier
     * @param ret The methods returner
     * @return The methods signature.
     * @see getMethodReturn, getMethodIdentifier
     */
    public static String getMethodSig(String identifier, String ret){
        // Everything *after* open parens.
        String sig = identifier.substring(identifier.indexOf('('));
        return sig + ret;
    }
    
    /**
     * Gets a methods returner.
     * 
     * The returner is essentially the methods return type.
     * 
     * The returner is defined as \1 in the regular expression
     * \(.*\)(.+) for the methods signature.
     * @param m The method.
     * @return Its returner.
     */
    public static String getMethodReturn(Method m){
        String sig = m.getSignature();
        // Get everything *after* close parens.
        return sig.substring(sig.indexOf(')') + 1);
    }
    
    /**
     * Gets a methods identifier.
     * 
     * The identifier is essentially the signature without return type and the
     * name.
     * 
     * The identifier is defined as method_name + \1 in the regular expression
     * (\(.*\)).+
     * @param m
     * @return
     */
    public static String getMethodIdentifier(Method m){
        String sig = m.getSignature();
        // Get everything *before* close parens.
        sig = sig.substring(0, sig.indexOf(')') + 1);
        return m.getName() + sig;
    }
    
    public static String getMethodIdentifier(ConstantCP c, ConstantPool cp){
        ConstantNameAndType cnat =
                (ConstantNameAndType)cp.getConstant(c.getNameAndTypeIndex());
        String sig = cnat.getSignature(cp);
        sig = sig.substring(0, sig.indexOf(')') + 1);
        return cnat.getName(cp) + sig;
    }
    
    /**
     * Gets the interface names of a class in the way they are stored in the
     * constant pool.
     * 
     * JavaClass's method is slightly different, but leads to interfaces not
     * being found in the cpool.
     * @param j The class.
     * @return The interfaces implemented by the class.
     */
    public static String[] getInterfaceNamesProper(JavaClass j){
        ConstantPool cpool = j.getConstantPool();
        int[] indexes = j.getInterfaceIndices();
        String[] names = new String[indexes.length];
        for(int i = 0; i < indexes.length; i++)
            names[i] = cpool.getConstantString(indexes[i],
                    Constants.CONSTANT_Class);
        return names;
    }
    
    /**
     * Loops an input stream into an output stream and closes both.
     * @param in
     * @param out
     * @throws IOException
     */
    public static void loop(InputStream in, OutputStream out)
            throws IOException{
        loop(in, out, true);
    }
    
    /**
     * Loops an input stream into an output stream and optionally closes both.
     * @param in
     * @param out
     * @param close
     * @throws IOException
     */
    public static void loop(InputStream in, OutputStream out, boolean close)
            throws IOException{
        byte[] buffer = new byte[1 << 10];
        for(int len = in.read(buffer); len > 0; len = in.read(buffer))
            out.write(buffer, 0, len);
        if(!close)
            return;
        in.close();
        out.close();
    }
    
    /**
     * 
     * @param from
     * @param to
     * @return The relative path from "from" to "to".
     */
    public static String getRelativePath(File from, File to){
        return from.toURI().relativize(to.toURI()).getPath();
    }
    
    /**
     * 
     * @param dir
     * @return All file paths in "dir" and its subdirectories.
     */
    public static List<String> listNamesIn(File dir){
        return listNamesIn(dir, dir);
    }
    
    /**
     * 
     * @param root
     * @param dir
     * @return All file paths in "dir" and its subdirectories, relative to
     *     "root".
     */
    private static List<String> listNamesIn(File root, File dir){
        List<String> ret = new ArrayList<String>();
        for(File f : dir.listFiles()){
            if(f.isDirectory())
                ret.addAll(listNamesIn(root, f));
            else
                ret.add(getRelativePath(root, f));
        }
        return ret;
    }
    
    /**
     *
     * @return A temp directory.
     * @throws IOException
     */
    public static File getTempDir() throws IOException{
        // So no, this isn't elegant, but really, why is it that whenever
        // I look up something basic like this, all suggestions I get are
        // either a minorly buggy code like this, or some massive third-party
        // library...
        File f = File.createTempFile("milk", "");
        f.delete();
        f.mkdir();
        return f;
    }
    
    /**
     * 
     * @param f1
     * @param f2
     * @return Whether "f1" and "f2" are content equal or not.
     * @throws IOException
     */
    public static boolean fileEquals(File f1, File f2) throws IOException{
        if(f1 == f2)
            return true;
        if(f1 == null || f2 == null)
            return false;
        if(!f1.isFile() || !f2.isFile())
            return false;
        InputStream i1 = new FileInputStream(f1);
        InputStream i2 = new FileInputStream(f2);
        byte[] buff1 = new byte[1 << 10];
        byte[] buff2 = new byte[1 << 10];
        int len1;
        int len2;
        while(true){
            len1 = i1.read(buff1);
            len2 = i2.read(buff2);
            if(len1 != len2){
                i1.close();
                i2.close();
                return false;
            }
            if(len1 == 0)
                break;
            if(!Arrays.equals(buff1, buff2)){
                i1.close();
                i2.close();
                return false;
            }
        }
        i1.close();
        i2.close();
        return true;
    }
    
    /**
     * Extracts a zipfile into a directory.
     * @param zip The zipfile.
     * @param dir The directory.
     * @throws IOException
     */
    public static void extractZip(File zip, File dir) throws IOException{
        if(!dir.exists())
            dir.mkdirs();
        ZipInputStream in = new ZipInputStream(new FileInputStream(zip));
        for(ZipEntry e = in.getNextEntry(); e != null; e = in.getNextEntry()){
            if(e.isDirectory())
                continue;
            File outF = new File(dir, e.getName());
            outF.getParentFile().mkdirs();
            OutputStream out = new FileOutputStream(outF.getAbsoluteFile());
            loop(in, out, false);
            out.close();
            in.closeEntry();
        }
        in.close();
    }
    
    /**
     * Extracts a zipfile.
     * @param zip The zipfile.
     * @return The directory into which it gets extracted.
     * @throws IOException
     */
    public static File extractZip(File zip) throws IOException{
        File outRoot = getTempDir();
        extractZip(zip, outRoot);
        return outRoot;
    }
    
    /**
     * Packs a zipfile.
     * @param zip The zipfile.
     * @param dir The directory to pack.
     * @throws IOException
     */
    public static void packZip(File zip, File dir) throws IOException{
        ZipOutputStream out = new ZipOutputStream(new FileOutputStream(zip));
        for(String name : listNamesIn(dir)){
            ZipEntry entry = new ZipEntry(name);
            out.putNextEntry(entry);
            InputStream in = new FileInputStream(new File(dir, name));
            loop(in, out, false);
            in.close();
            out.closeEntry();
        }
        out.close();
    }
    
    /**
     * Deletes "dir" recursively.
     * @param dir The directory to delete.
     * @throws IOException
     */
    public static void remDir(File dir) throws IOException{
        for(File f : dir.listFiles()){
            if(f.isDirectory())
                remDir(f);
            else
                f.delete();
        }
        dir.delete();
    }
    
    /**
     * 
     * @param m A method.
     * @return The amount of variables with a fixed local variable index for m.
     */
    public static int getFixedLocalVariableLength(Method m){
        int currCount = 0;
        if(!m.isStatic())
            currCount++;
        for(Type t : m.getArgumentTypes()){
            currCount += t.getSize();
        }
        return currCount;
    }
    
    /**
     * Transforms an integer list into an int array.
     * @param list The list to transform.
     * @return The int array.
     */
    public static int[] toIntArray(List<Integer> list){
        int[] ret = new int[list.size()];
        for(int i = 0; i < ret.length; i++)
            ret[i] = list.get(i);
        return ret;
    }
    
}
